﻿// The MIT License (MIT)
//
// Copyright (c) 2015 FramTack, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//
// For more information visit the project home page at http://solutionfamily.codeplex.com or the owner's site at http://solution-family.com
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Threading;
using System.Net;
using System.Diagnostics;
using OpenNETCF;

namespace SolutionFamily.IO.Simma
{
    public class VNA232
    {
        public event EventHandler Heartbeat;

        public const byte SourceAddress = 252;

        private SerialPort m_port;

        private Simma1708 m_1708;
        private SimmaCAN m_can0;
        private SimmaCAN m_can1;

        public VNA232(string port)
        {
            m_port = new SerialPort(port, 115200);

            m_1708 = new Simma1708(this);

            m_can0 = new SimmaCAN(this, 0);
            m_can1 = new SimmaCAN(this, 1);
        }

        public IJ1939Bus CAN0
        {
            get { return m_can0; }
        }

        public IJ1939Bus CAN1
        {
            get { return m_can1; }
        }

        public IJ1708Bus J1708
        {
            get { return m_1708; }
        }

        public bool Connect()
        {
            if (!SerialPort.GetPortNames().Contains(m_port.PortName, StringComparer.InvariantCultureIgnoreCase))
            {
                Debug.WriteLine("Invalid COM port");
                // TODO: log this, maybe set some data value or raise an event?
                return false;
            }

            try
            {
                m_port.Open();

                new Thread(ReadProc)
                {
                    IsBackground = true,
                    Name = "Simma Reader"
                }
                .Start();

                //            m_port.DataReceived += new SerialDataReceivedEventHandler(m_port_DataReceived);
                m_port.ErrorReceived += new SerialErrorReceivedEventHandler(m_port_ErrorReceived);

                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool IsConnected
        {
            get { return m_port.IsOpen; }
        }

        void m_port_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
        {
        }

        private void ReadProc(object o)
        {
            // frames start with 192, then 2 bytes of length (big endian), then data and 1 byte checksum

            int lengthMSB;
            int lengthLSB;
            int b;

            while(true)
            {
                // find a start sentinel
                do
                {
                    try
                    {
                        b = m_port.ReadByte();
                    }
                    catch
                    {
                        return;
                    }
                } while (b != 192);

                // now pull the length
                do
                {
                    lengthMSB = m_port.ReadByte();
                } while (lengthMSB < 0);
                do
                {
                    lengthLSB = m_port.ReadByte();
                } while (lengthLSB < 0);

                var length = ((lengthMSB << 8) | lengthLSB);

                // TODO: unstuff data

                // now pull all of the data
                var data = new byte[length];

                int read = 0;
                int remaining = length;
                int offset = 0;

                do
                {
                    read = m_port.Read(data, offset, remaining);
                    offset += read;
                    remaining -= read;
                } while (remaining > 0);

                var cs = data[data.Length - 1];

                // todo: verify the checksum
                var e = lengthMSB & 0xff;
                e = (e + lengthLSB) & 0xff;
                e = TwosComplement((byte)e, data, 0, data.Length - 1);
                e = 256 - e;

                if (cs != e)
                {
                    Debug.WriteLine("Bad checksum: " + data[0].ToString());
                }
                else
                {
                    ProcessIncoming(data);
                }

                Thread.Sleep(1);
            }
            
        }

        private void ProcessIncoming(byte[] data)
        {
            var id = (MessageIdentifier)data[0];

//            Debug.WriteLine("Simma Rx: " + id.ToString());

            switch (id)
            {
                case MessageIdentifier.Ack:
//                    Debug.WriteLine("Simma Rx: " + id.ToString());
                    // TODO: do we care?
                    LastMessageReceived = DateTime.Now;
                    break;
                case MessageIdentifier.RxJ1587:
                    var mid = data[1];
                    var pid = (ushort)((data[2] << 8) | data[3]);
                    var payload = new byte[data.Length - 4 - 1];
                    Buffer.BlockCopy(data, 4, payload, 0, payload.Length);

                    var j1708 = new J1708ReceiveMessage(data[1], pid, payload);
                    m_1708.RaiseMessageReceived(j1708);

                    LastMessageReceived = DateTime.Now;
                    return;
                case MessageIdentifier.Stats:
                    var s = new VNAStats(data);
                    // TODO: update some internal stuff?
                    HardwareVersion = s.HardwareVersion;
                    SoftwareVersion = s.SoftwareVersion;
                    LastMessageReceived = DateTime.Now;
                    J1939FramesReceived = s.TotalCANFrames;
                    J1708MessagesReceived = s.Total1708Messages;

                    Heartbeat.Fire(this, EventArgs.Empty);

                    break;
                case MessageIdentifier.RX_J1939:
                    var port = data[1];
                    var pgn = (data[2] << 16) | (data[3] << 8) | data[4];
                    var msgdata = new byte[data.Length - 8 - 1]; // skip header, don't include checksum
                    Buffer.BlockCopy(data, 8, msgdata, 0, msgdata.Length);

                    var j1939 = new J1939ReceiveMessage(pgn, data[5], data[6], data[7], msgdata);

                    switch (data[1])
                    {
                        case 0:
                            m_can0.RaiseMessageReceived(j1939);
                            break;
                        default:
                            m_can1.RaiseMessageReceived(j1939);
                            break;
                    }

                    LastMessageReceived = DateTime.Now;

                    break;
                default:
                    throw new NotSupportedException();
            }
        }

        public int HardwareVersion { get; private set; }
        public int SoftwareVersion { get; private set; }
        public DateTime LastMessageReceived { get; private set; }
        public int J1939FramesReceived { get; private set; }
        public int J1708MessagesReceived { get; private set; }

        private List<byte> m_txbuffer = new List<byte>();

        internal void Send(byte[] data)
        {
            if (m_port.IsOpen)
            {
                lock (m_txbuffer)
                {
                    m_txbuffer.Clear();

                    // check for byte stuffing after the first 3 bytes (sentinel and length)
                    for (int i = 0; i < data.Length; i++)
                    {
                        if (i > 2)
                        {
                            /* 0xC0 is sent as 0xDB, 0xDC
                               0xDB is sent as 0xDB, 0xDD */
                            switch (data[i])
                            {
                                case 0xC0:
                                    m_txbuffer.Add(0xDB);
                                    m_txbuffer.Add(0xDC);
                                    break;
                                case 0xDB:
                                    m_txbuffer.Add(0xDB);
                                    m_txbuffer.Add(0xDD);
                                    break;
                                default:
                                    m_txbuffer.Add(data[i]);
                                    break;
                            }
                        }
                        else
                        {
                            m_txbuffer.Add(data[i]);
                        }
                    }
                }

                // the VNA uses byte stuffing, so watch for that
                m_port.Write(m_txbuffer.ToArray(), 0, m_txbuffer.Count);
            }
            else
            {
                Debug.WriteLine("Attempt to send on closed port");
            }
        }

        internal static byte TwosComplement(byte seed, byte[] buffer, int offset, int length)
        {
            byte checksum = seed;

            for (int i = offset; i < offset + length; i++)
            {
                checksum = (byte)((checksum + buffer[i]) & 0xFF);
            }

            return checksum;
        }
    }
}
